package com.aricwang.easyprofile.service;

import java.util.Calendar;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.location.Location;
import android.location.LocationManager;
import android.media.AudioManager;
import android.net.wifi.WifiManager;
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
import android.provider.Settings;
import android.text.format.Time;
import android.util.Log;

import com.aricwang.easyprofile.EasyProfileActivity;
import com.aricwang.easyprofile.R;
import com.aricwang.easyprofile.contentprovider.RuleContentProvider;
import com.aricwang.easyprofile.database.RuleTable;
import com.aricwang.easyprofile.place.Place;
import com.aricwang.easyprofile.profile.Profile;
import com.aricwang.easyprofile.receiver.ChargingReceiver;
import com.aricwang.easyprofile.receiver.TimeChangedReceiver;
import com.aricwang.easyprofile.rule.Rule;

public class ProfileService extends Service implements android.location.LocationListener {
	public static final String PROFILE_BROADCAST = "com.aricwang.easyprofile.service.PROFILE_BROADCAST";
	
	public static final int SERVICE_START = 1;
	public static final int SERVICE_FORCE_PROFILE = 2;
	public static final int SERVICE_UNFORCE_PROFILE = 3;
	public static final int SERVICE_APPLY_PROFILE = 4;
	public static final int SERVICE_UNAPPLY_PROFILE = 5;
	public static final int	SERVICE_CHARGING_CHANGED = 6;
	public static final int SERVICE_CONFIG_CHANGED = 7;
	public static final int SERVICE_TIME_CHANGED = 8;
	
	public static final int MODE_FORCED = 1;
	public static final int MODE_MANUAL = 2;
	public static final int MODE_AUTO   = 3;
	
	private static final int ONGOING_NOTIFICATION = 1;
	private static final int DATE_NOTIFICATION = 2;

	private int mMode = MODE_AUTO;	// profile mode: forced, manual or auto
	private int mProfileId = 0;	// forced or manual profile id
	
	private int mRuleId = 0;	// the current rule id
	
	private Profile mProfile = null;	// the current profile
	
	private PowerManager.WakeLock mWakeLock;
	private boolean mStarted = false;
	private boolean mCharging = false;
	
	private Location mLocation;
	
	private NotificationManager mNotificationManager;

	private TimeChangedReceiver mTimeReceiver;
	private ChargingReceiver mChargingReceiver;
	
	private int mDayOfMonth;
	

	/////////////////////////////////////////////////////////////////////////////////////////
	// Static functions
	/////////////////////////////////////////////////////////////////////////////////////////
	
	static public void startService(Context context) {
		Intent intent = new Intent(context, ProfileService.class);
		intent.putExtra("Request", SERVICE_START);
		context.startService(intent);		
	}
	
	static public void forceProfile(Context context, int profileId) {
		Intent intent = new Intent(context, ProfileService.class);
		intent.putExtra("Request", SERVICE_FORCE_PROFILE);
		intent.putExtra("ProfileId", profileId);
		context.startService(intent);		
	}
	
	static public void unforceProfile(Context context, int profileId) {
		Intent intent = new Intent(context, ProfileService.class);
		intent.putExtra("Request", SERVICE_UNFORCE_PROFILE);
		intent.putExtra("ProfileId", profileId);
		context.startService(intent);		
	}

	static public void applyProfile(Context context, int profileId) {
		Intent intent = new Intent(context, ProfileService.class);
		intent.putExtra("Request", SERVICE_APPLY_PROFILE);
		intent.putExtra("ProfileId", profileId);
		context.startService(intent);		
	}
	
	static public void unapplyProfile(Context context, int profileId) {
		Intent intent = new Intent(context, ProfileService.class);
		intent.putExtra("Request", SERVICE_UNAPPLY_PROFILE);
		intent.putExtra("ProfileId", profileId);
		context.startService(intent);		
	}

	static public void notifyChargingState(Context context, boolean charging) {
		Intent intent = new Intent(context, ProfileService.class);
		intent.putExtra("Request", SERVICE_CHARGING_CHANGED);
		intent.putExtra("Charging", charging);
		context.startService(intent);		
	}
	
	static public void notifyTimeChanged(Context context) {
		Intent intent = new Intent(context, ProfileService.class);
		intent.putExtra("Request", SERVICE_TIME_CHANGED);
		context.startService(intent);		
	}

	static public void notifyConfigChanged(Context context) {
		Intent intent = new Intent(context, ProfileService.class);
		intent.putExtra("Request", SERVICE_CONFIG_CHANGED);
		context.startService(intent);		
	}

	/////////////////////////////////////////////////////////////////////////////////////////
	// Override functions
	/////////////////////////////////////////////////////////////////////////////////////////

	public void onCreate() {
		// Have to get calendar every time. Otherwise the date is wrong.
		Calendar c= Calendar.getInstance();
		mDayOfMonth = c.get(Calendar.DAY_OF_MONTH);
		
		PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
		mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, (String)getText(R.string.app_name));
		
		mCharging = getChargingStatus();
		
		mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

		registerTimeChangedReceiver();
		registerChargingReceiver();
		
		// get the current location
        LocationManager locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE); 
        mLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); 
		
        // register location listener
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 60000, 200, this);
	}

	public int onStartCommand(Intent intent, int flags, int startId) {
		int request = intent.getIntExtra("Request", SERVICE_START);
		int id;

		switch(request) {
		case SERVICE_START:
			if (mStarted == false) {
				mStarted = true;
				sendNotification("Service started", "Service started");
			} else {
				sendBroadcastMsg(mMode, mProfile);
			}
			break;
			
		case SERVICE_FORCE_PROFILE:
			id = intent.getIntExtra("ProfileId", -1);
			if (id > 0) {
				Profile p = Profile.getProfile(this, id);
				if (p != null) {
					mMode = MODE_FORCED;
					mProfileId = id;
					
					applyProfile(p);
					
					sendBroadcastMsg(mMode, mProfile);
				}
			}
			break;

		case SERVICE_UNFORCE_PROFILE:
			if (mMode == MODE_FORCED) {
				id = intent.getIntExtra("ProfileId", -1);
				if (id > 0 && id == mProfileId) {
					mMode = MODE_AUTO;
					mProfileId = 0;
					
					// NOTE:
					// when quit from forced mode, we set the mRuleId to 0 so 
					// that it can always select a rule and apply it. 
					mRuleId = 0;
					
					scanRules();

					sendBroadcastMsg(mMode, mProfile);
				}
			}
			break;
			
		case SERVICE_APPLY_PROFILE:
			// forced mode overwrites the manual mode
			if (mMode != MODE_FORCED) {
				id = intent.getIntExtra("ProfileId", -1);
				if (id > 0) {
					Profile p = Profile.getProfile(this, id);
					if (p != null) {
						mMode = MODE_MANUAL;
						mProfileId = id;
						
						applyProfile(p);

						sendBroadcastMsg(mMode, mProfile);
					}
				}
			}
			break;
			
		case SERVICE_UNAPPLY_PROFILE:
			if (mMode == MODE_MANUAL) {
				id = intent.getIntExtra("ProfileId", -1);
				if (id > 0 && id == mProfileId) {
					Profile p = Profile.getProfile(this, id);
					if (p != null) {
						mMode = MODE_AUTO;
						mProfileId = 0;

						// NOTE:
						// when quit from manual mode, we set the mRuleId to 0 so 
						// that it can always select a rule and apply it. 
						mRuleId = 0;
						
						scanRules();

						sendBroadcastMsg(mMode, mProfile);
					}
				}
			}
			break;

		case SERVICE_CHARGING_CHANGED:
			mCharging = intent.getBooleanExtra("Charging", false);
//			Toast.makeText(this, "Charging " + mCharging, Toast.LENGTH_SHORT).show();
			scanRules();
			break;
			
		case SERVICE_TIME_CHANGED:
//			Toast.makeText(this, "time changed", Toast.LENGTH_SHORT).show();
			scanRules();
			checkDateChange();
			break;
			
		case SERVICE_CONFIG_CHANGED:
//			Toast.makeText(this, "config changed", Toast.LENGTH_SHORT).show();
			scanRules();
			break;
			
		default:
			break;
		}

		// If we get killed, after returning from here, restart
		return START_STICKY;
	}

	public IBinder onBind(Intent intent) {
		// We don't provide binding, so return null
		return null;
	}

	public void onDestroy() {
		unregisterTimeChangedReceiver();
		unregisterChargingReceiver();
        
		// register location listener
		LocationManager locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE); 
        locationManager.removeUpdates(this);
		
		if (mWakeLock.isHeld())
			mWakeLock.release();
		
		stopForeground(true);
		
		cancelNotification();
		
	}
	
	//////////////////////////////////////////////////////////////////////////////////////////////
	// Functions of LocationListener interface
	//////////////////////////////////////////////////////////////////////////////////////////////
	public void onLocationChanged(Location location) {
//		Toast.makeText(this, "Location changed: " + location.toString(), Toast.LENGTH_LONG).show();
//		Log.i("T", "location changed: " + location.getLatitude() + ", " + location.getLongitude());
		mLocation = location;
		scanRules();
	}
	
	public void onProviderDisabled(String provider) {
//		Toast.makeText(this, "provider disabled: " + provider, Toast.LENGTH_LONG).show();
		Log.i("T", "Providoer " + provider + " disabled");
	}
	
	public void onProviderEnabled(String provider) {
		Log.i("T", "Providoer " + provider + " enabled");
//		Toast.makeText(this, "provider enabled: " + provider, Toast.LENGTH_LONG).show();
	}
	
	public void onStatusChanged(String provider, int status, Bundle extras) {
//		Toast.makeText(this, provider + " status changed: " + status + ", extras: " + extras.toString(), Toast.LENGTH_LONG).show();
		Log.i("T", "Providoer " + provider + " status " + status);
	}

	//////////////////////////////////////////////////////////////////////////////////////////////
	// Private functions
	//////////////////////////////////////////////////////////////////////////////////////////////
	
	private boolean getChargingStatus() {
		IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
		Intent batteryStatus = registerReceiver(null, ifilter);
		
		// Are we charging / charged?
		int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
		boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
		                     status == BatteryManager.BATTERY_STATUS_FULL;
		
		return isCharging;
	}
	
	// scan all the configured rules and apply the first one which conditions are true.
	// if no one is satisfied, do nothing.
	// if manual profile is forced, do nothing.
	// Return values:
	// if a new profile applied, return true. otherwise, return false.
	private boolean scanRules() {
		if (mMode == MODE_FORCED)
			return false;
		
		Rule r = new Rule();
		Profile p = null;
		
		Cursor c = null;
		try {
			String sortOrder = RuleTable.COLUMN_PRIORITY + " desc";
			c = getContentResolver().query(RuleContentProvider.CONTENT_URI, RuleTable.ALL_COLUMNS, 
					null, null, sortOrder); 
		} catch (Exception e){
			throw  new IllegalArgumentException("ERROR");
		};
		
		if (c == null)
			return false;
		
		boolean ret = false;
		
		int count = c.getCount();
		
		if (count > 0) {
			// must move to first, otherwise, get exception
			c.moveToFirst();
			
			int i;
			for(i = 0; i < count; i++, c.moveToNext()) {
				// get the item from database
				r.mRuleId = c.getInt(c.getColumnIndex(RuleTable.COLUMN_ID));
				r.mRuleName = c.getString(c.getColumnIndex(RuleTable.COLUMN_NAME));
				r.mPriority = c.getInt(c.getColumnIndex(RuleTable.COLUMN_PRIORITY));
				r.mStartHour = c.getInt(c.getColumnIndex(RuleTable.COLUMN_TIMEFRAME_START_HOUR));
				r.mStartMinute = c.getInt(c.getColumnIndex(RuleTable.COLUMN_TIMEFRAME_START_MINUTE));
				r.mEndHour = c.getInt(c.getColumnIndex(RuleTable.COLUMN_TIMEFRAME_END_HOUR));
				r.mEndMinute = c.getInt(c.getColumnIndex(RuleTable.COLUMN_TIMEFRAME_END_MINUTE));
				r.mRepeat = c.getInt(c.getColumnIndex(RuleTable.COLUMN_TIMEFRAME_REPEAT));
				r.mCharging = c.getInt(c.getColumnIndex(RuleTable.COLUMN_CHARGING));
				r.mPlaceId = c.getInt(c.getColumnIndex(RuleTable.COLUMN_PLACE));
				r.mProfileId = c.getInt(c.getColumnIndex(RuleTable.COLUMN_PROFILE));
				
				p = checkRule(r);
				
				// If a rule is valid, terminate the scan.
				if (p != null) {
					// if the new valid rule is different from the current rule, apply it.
					if (mRuleId != r.mRuleId) {
						// set to auto mode
						mMode = MODE_AUTO;
						mProfileId = 0;
						
						mRuleId = r.mRuleId;
						applyProfile(p);
						
						ret = true;
						
						sendBroadcastMsg(mMode, p);
					}

					// terminate the scan
					break;
				}
			}
			
			// no rule applied
			if (i == count) {
				sendNotification("No rule satisfied!", "Profile: " + mProfile.mProfileName);
			}
		}
	
		c.close();
		
		return ret;
	}
	
	// check the specific rule against current situation. If it's satisfied, return the profile,
	// Otherwise, return null.
	private Profile checkRule(Rule r) {
		// check timeframe
		if (r.mRepeat > 0) {
			Time now = new Time(Time.getCurrentTimezone());
			now.setToNow();
			
			int start = r.mStartHour*100 + r.mStartMinute;
			int end = r.mEndHour*100 + r.mEndMinute;
			int time = now.hour*100 + now.minute;
			
			boolean overnight = (start >= end);
			int weekday = TimeToWeekday(now.weekDay);

			// check time
			if (overnight == false) { 
				if (time < start || time > end)
					return null;
			} else {
				if (time > end && time < start)
					return null;
				
				if (time <= end) {
					weekday = TimeToWeekday((now.weekDay + 6) % 7);
				}
			}
			
			// check date
			if ((r.mRepeat & weekday) == 0)
				return null;
		}
		
		// check charging status 
		if ((r.mCharging == Rule.CHARGING && mCharging == false)
				|| (r.mCharging == Rule.NOT_CHARGING && mCharging == true)) 
		{
			return null;
		}
		
		// checking place
		if (r.mPlaceId > 0) {
			Place place = Place.getPlace(this, r.mPlaceId);
			if (isAtPlace(place) == false) 
				return null;
		}
		
		// get profile here
		return Profile.getProfile(this, r.mProfileId);
	}
	
	private int TimeToWeekday(int day) {
		switch (day) {
		case Time.SUNDAY: 		return Rule.SUN;
		case Time.MONDAY:		return Rule.MON;
		case Time.TUESDAY:		return Rule.TUE;
		case Time.WEDNESDAY:	return Rule.WED;
		case Time.THURSDAY:		return Rule.THU;
		case Time.FRIDAY:		return Rule.FRI;
		case Time.SATURDAY:		return Rule.SAT;
		default:				return 0;
		}
	}
	
	// send a notification msg to indicate the current profile applied
	private void sendNotification(String msg, String lastEvent) {
		cancelNotification();
		
		int[] dates = new int[] 
			{
				R.drawable.ic_01, R.drawable.ic_02, R.drawable.ic_03, R.drawable.ic_04, R.drawable.ic_05,
				R.drawable.ic_06, R.drawable.ic_07, R.drawable.ic_08, R.drawable.ic_09, R.drawable.ic_10,
				R.drawable.ic_11, R.drawable.ic_12, R.drawable.ic_13, R.drawable.ic_14, R.drawable.ic_15,
				R.drawable.ic_16, R.drawable.ic_17, R.drawable.ic_18, R.drawable.ic_19, R.drawable.ic_20,
				R.drawable.ic_21, R.drawable.ic_22, R.drawable.ic_23, R.drawable.ic_24, R.drawable.ic_25,
				R.drawable.ic_26, R.drawable.ic_27, R.drawable.ic_28, R.drawable.ic_29, R.drawable.ic_30,
				R.drawable.ic_31
			};
		
		int ic = dates[mDayOfMonth - 1];

		Notification notification;

		notification = new Notification(ic, msg, System.currentTimeMillis());

		Intent notificationIntent = new Intent(this, EasyProfileActivity.class);
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
		notification.setLatestEventInfo(this, getText(R.string.app_name), lastEvent,
				contentIntent);

		// start the service in foreground.
		startForeground(ONGOING_NOTIFICATION, notification);
	}

	private void cancelNotification() {
		mNotificationManager.cancelAll();
	}
	
	// apply the specified profile to the system
	private void applyProfile(Profile profile) {
		mProfile = profile;

		setVolumes(mProfile.mVolumeCall);
		setVibrate(mProfile.mVibrate);

		// set the cell first, and then Wifi, bluetooth
		setCell(mProfile.mCell);
		setWifi(mProfile.mWifi);
		setBluetooth(mProfile.mBluetooth);

		// not working ones
		setScreenTimeout(mProfile.mTimeout);
		
		String mode;
		if (mMode == MODE_FORCED)
			mode = "Forced";
		else if (mMode == MODE_MANUAL)
			mode = "Manual";
		else
			mode = "Auto";
		
		sendNotification(mProfile.mProfileName + " applied", "Profile: " + mProfile.mProfileName + " (" + mode + ")");
	}

	private void setVolumes(int volume) {
		if (volume == Profile.NOCHANGE)
			return;
		
		AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
//		am.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
//				am.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL) * volume
//						/ 100, 0);
		am.setStreamVolume(AudioManager.STREAM_ALARM,
				am.getStreamMaxVolume(AudioManager.STREAM_ALARM) * volume / 100,
				0);
		am.setStreamVolume(AudioManager.STREAM_RING,
				am.getStreamMaxVolume(AudioManager.STREAM_RING) * volume
						/ 100, 0);
		am.setStreamVolume(AudioManager.STREAM_SYSTEM,
				am.getStreamMaxVolume(AudioManager.STREAM_SYSTEM) * volume
						/ 100, 0);
		am.setStreamVolume(AudioManager.STREAM_NOTIFICATION,
				am.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION)
						* volume / 100, 0);
	}

	private void setVibrate(int value) {
		AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
		if (value == Profile.ON) {
			am.setVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION,
					AudioManager.VIBRATE_SETTING_ON);
			am.setVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER,
					AudioManager.VIBRATE_SETTING_ON);
		} else if (value == Profile.OFF) {
			am.setVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION,
					AudioManager.VIBRATE_SETTING_OFF);
			am.setVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER,
					AudioManager.VIBRATE_SETTING_OFF);
		}
	}

	private void setCell(int value) {
		
		
		int flight = 0;
		if (value == Profile.NOCHANGE) {
			return;
		} else if (value == Profile.FLIGHT || value == Profile.OFF) {
			flight = 1;
		} else if (value == Profile.ON) {
			flight = 0;
		}
		
		// use airplane mode to disable the cell radio
		Settings.System.putInt(getContentResolver(),
				Settings.System.AIRPLANE_MODE_ON, flight);

		// Post an intent to reload.
		Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
		intent.putExtra("state", flight);
		sendBroadcast(intent);
	}
	
	private void setWifi(int value) {
		boolean status = false;
		if (value == Profile.NOCHANGE) {
			return;
		} else if (value == Profile.FLIGHT || value == Profile.OFF) {
			status = false;
		} else if (value == Profile.ON) {
			status = true;
		}
		
		WifiManager wm = (WifiManager) getSystemService(WIFI_SERVICE);
		wm.setWifiEnabled(status);
	}

	private void setBluetooth(int value) {
		BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
		if (value == Profile.NOCHANGE) {
			return;
		} else if (value == Profile.FLIGHT || value == Profile.OFF) {
			adapter.disable();
		} else if (value == Profile.ON) {
			adapter.enable();
		}
	}

	// set screen timeout
	private void setScreenTimeout(int value) {
		if (value == Profile.NOCHANGE)
			return;
		
		if (value == 0) {
			if (mWakeLock.isHeld() == false)
				mWakeLock.acquire();
		} else {
			if (mWakeLock.isHeld() == true)
				mWakeLock.release();

			Settings.System.putInt(getContentResolver(),
					Settings.System.SCREEN_OFF_TIMEOUT, value * 1000);
		}
	}

	private boolean isAtPlace(Place p) {
		Location loc = new Location(LocationManager.NETWORK_PROVIDER);
		loc.setLatitude(p.mLatitude);
		loc.setLongitude(p.mLongitude);	
		
		if (mLocation == null || loc == null)
			return false;
		
		int distance = (int)mLocation.distanceTo(loc); 
		
		return (distance < p.mRadius);
	}
	
	// send broadcast msg when a new profile is applied.
	private void sendBroadcastMsg(int mode, Profile profile) {
		Intent intent = new Intent(PROFILE_BROADCAST);
		intent.putExtra("Mode", mode);
		intent.putExtra("ProfileId", profile.mProfileId);
		intent.putExtra("ProfileName", profile.mProfileName);
		
		sendBroadcast(intent);
	}
	
	private void registerTimeChangedReceiver() {
		mTimeReceiver = new TimeChangedReceiver();
		
		// register the time tick receiver and time changed receiver. They don't work with manifest file.
		IntentFilter filter = new IntentFilter();
		filter.addAction(Intent.ACTION_TIME_TICK);
		filter.addAction(Intent.ACTION_TIME_CHANGED);
		registerReceiver(mTimeReceiver, filter);
	}
	
	private void unregisterTimeChangedReceiver() {
		unregisterReceiver(mTimeReceiver);
	}
	
	private void registerChargingReceiver() {
		mChargingReceiver = new ChargingReceiver();
		
		// register the time tick receiver and time changed receiver. They don't work with manifest file.
		IntentFilter filter = new IntentFilter();
		filter.addAction(Intent.ACTION_POWER_CONNECTED);
		filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
		registerReceiver(mChargingReceiver, filter);
	}
	
	private void unregisterChargingReceiver() {
		unregisterReceiver(mChargingReceiver);
	}

	private void checkDateChange() {
		Calendar c= Calendar.getInstance();
		int day = c.get(Calendar.DAY_OF_MONTH);
		
		if (day != mDayOfMonth) {
			mDayOfMonth = day;
			
			sendNotification("Date changed!", "Profile: " + mProfile.mProfileName);
		}
	}
}
